package flydb

import (
	"errors"
	"io"
	"os"
	"sync"
)

type FlyDB struct {
	indexes map[string]int64
	dbFile  *DBFile
	dirPath string
	mu      sync.RWMutex
}

func Open(dirPath string) (*FlyDB, error) {
	if _, err := os.Stat(dirPath); os.IsNotExist(err) {
		if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {
			return nil, err
		}
	}

	dbFile, err := NewDBFile(dirPath)
	if err != nil {
		return nil, err
	}

	db := &FlyDB{
		dbFile:  dbFile,
		indexes: make(map[string]int64),
		dirPath: dirPath,
	}

	db.loadIndexesFromFile()
	return db, nil
}

func (db *FlyDB) loadIndexesFromFile() {
	if db.dbFile == nil {
		return
	}

	var offset int64
	for {
		e, err := db.dbFile.Read(offset)
		if err != nil {
			if err == io.EOF {
				break
			}
			return
		}

		db.indexes[string(e.Key)] = offset

		if e.Mark == DEL {
			delete(db.indexes, string(e.Key))
		}

		offset += e.Size()
	}
	return
}

func (db *FlyDB) Put(key []byte, value []byte) (err error) {
	if len(key) == 0 {
		return
	}

	db.mu.Lock()
	defer db.mu.Unlock()

	offset := db.dbFile.Offset
	entry := NewEntry(key, value, PUT)
	err = db.dbFile.Write(entry)

	db.indexes[string(key)] = offset
	return
}

func (db *FlyDB) exist(key []byte) (int64, error) {
	offset, ok := db.indexes[string(key)]
	if !ok {
		return 0, ErrKeyNotFound
	}
	return offset, nil
}

func (db *FlyDB) Get(key []byte) (val []byte, err error) {
	if len(key) == 0 {
		return
	}

	db.mu.Lock()
	defer db.mu.Unlock()

	offset, err := db.exist(key)
	if errors.Is(err, ErrKeyNotFound) {
		return
	}

	var e *Entry
	e, err = db.dbFile.Read(offset)
	if err != nil && err != io.EOF {
		return
	}

	if e != nil {
		val = e.Value
	}
	return
}

func (db *FlyDB) Del(key []byte) (err error) {
	if len(key) == 0 {
		return
	}

	db.mu.Lock()
	defer db.mu.Unlock()

	_, err = db.exist(key)
	if errors.Is(err, ErrKeyNotFound) {
		err = nil
		return
	}

	e := NewEntry(key, nil, DEL)
	err = db.dbFile.Write(e)
	if err != nil {
		return
	}

	delete(db.indexes, string(key))
	return
}

func (db *FlyDB) Close() error {
	if db.dbFile == nil {
		return ErrInvalidDBFile
	}
	return db.dbFile.File.Close()
}
